import { Buffer } from 'node:buffer';
import * as socketSyncBuffer from "socket-sync-buffer"
import iconv from "iconv-lite"

// Hierarchy

// 对象初始化 选项
export interface ImapSyncClientOptions extends socketSyncBuffer.options {
    user: string // 用户
    pass: string // 密码
    tryStartTLS?: boolean  // 如果服务器支持就启动 STARTTLS
    startTLS?: boolean // 是否启动 STARTTLS
    cmdIdInfo?: string // imap 命令 ID 的具体内容, 一般用于向服务器表明身份
}

// 回调函数类型, 记录通讯协议内容
export interface ReadWriteRecordHandler {
    (type: string, data: Buffer): void   // type: read/write
}

// 读取一行, 解析为一组 token
export interface ReadOneLineResult {
    tokens: Buffer[]
    extraDataLength: number // 最后一个token 形如: {123}
}

// 命令 status 结果解析
export interface MboxStatus {
    messages: number
    recent: number
    uidnext: number
    uidvalidity: number
    unseen: number
}

// 命令 list 返回的结果按行解析
export interface MboxAttrs {
    noinferiors: boolean
    noselect: boolean
    hierarchySplitor: string,
    inbox: boolean
    junk: boolean
    trash: boolean
    sent: boolean
    drafts: boolean
}

// 命令 select 返回的结果解析
export interface MboxSelect {
    exists: number
    recent: number
    uidvalidity: number
    uidnext: number
    highestmodseq: number
}

// 文件夹信息
export interface MboxInfo {
    mboxName: Buffer, // 文件夹名(Buffer), 返回的文件夹名字可能不是 imap-utf-7 编码
    mboxNameUtf8: string, // 文件夹名), 一定是 ""
    attrs: MboxAttrs
    status?: MboxStatus
    subscribed?: boolean
}

// 邮件标记
export interface MailFlags {
    answered?: boolean // 是否已回复
    seen?: boolean // 是否已读
    draft?: boolean // 是否草稿
    flagged?: boolean // 是否标记(星标)
    deleted?: boolean // 是否删除
}

// 邮件标记 + 邮件 UID, 用于邮件列表
export interface MailUidWithFlags extends MailFlags {
    uid: number
}

// uidplus 扩展, 移动/复制/上传的结果
export interface UidplusResult {
    uidvalidity: number,
    uid: number
}

// 字符集转码: imap-utf-7 => utf-8 
export function imapUtf7ToUtf8(str: string | Buffer): string {
    let bf: Buffer
    if (Buffer.isBuffer(str)) {
        bf = str
    } else {
        bf = Buffer.from(str)
    }
    let r = ""
    try {
        r = iconv.decode(bf, "utf7imap")
    } catch { }
    return r
}

// 字符集转码: utf-8 => imap-utf-7
export function utf8ToImapUtf7(str: string | Buffer): string {
    if (Buffer.isBuffer(str)) {
        str = str.toString()
    }
    let r = ""
    try {
        r = iconv.encode(str, "utf7imap").toString()
    } catch { }
    return r
}

export class imapSyncClient {
    char_newline: number
    char_backslash: number
    char_quot: number
    char_blank: number
    char_star: number
    debugMode: boolean
    readWriteRecorder: ReadWriteRecordHandler | null
    lastReadedBuffer: Buffer
    tag_id: number
    user: string
    pass: string
    tryStartTLS: boolean
    startTLS: boolean
    cmdIdInfo: string
    socket: socketSyncBuffer.socketSyncBuffer
    logicErrorFlag: boolean
    errorFlag: boolean
    passwordErrorFlag: boolean
    needCloseConnection: boolean
    resultStatus: string
    capability: string
    lastSelectedMbox: Buffer
    lastSelectedResult: MboxSelect | null
    maybeHaveNewMailHandler: any = null

    constructor(options: ImapSyncClientOptions) {
        this.char_newline = "\n".charCodeAt(0)
        this.char_backslash = "\\".charCodeAt(0)
        this.char_quot = "\"".charCodeAt(0)
        this.char_blank = " ".charCodeAt(0)
        this.char_star = "*".charCodeAt(0)
        this.debugMode = false
        this.readWriteRecorder = null
        this.lastReadedBuffer = Buffer.allocUnsafe(0)
        this.tag_id = 1
        this.user = options.user
        this.pass = options.pass
        this.tryStartTLS = false
        this.startTLS = false
        if (options.tryStartTLS) {
            this.tryStartTLS = true
        }
        if (options.startTLS) {
            this.startTLS = true
        }
        this.cmdIdInfo = ""
        if (options.cmdIdInfo !== undefined) {
            this.cmdIdInfo = options.cmdIdInfo
        }
        this.socket = new socketSyncBuffer.socketSyncBuffer(options)
        this.errorFlag = false
        this.logicErrorFlag = false
        this.passwordErrorFlag = false
        this.needCloseConnection = false
        this.resultStatus = ""
        this.capability = ""
        this.lastSelectedMbox = Buffer.allocUnsafe(0)
        this.lastSelectedResult = null
    }
    setDebugMode(tf = true) {
        this.debugMode = tf
    }

    // 设置回调函数,记录通讯协议
    setReadWriteRecordHandler(handler: ReadWriteRecordHandler) {
        this.readWriteRecorder = handler
    }

    // 设置回调函数, 在操作中，是否有可能有新的信件
    setMaybeHaveNewMailHandler(handler: (pathname: Buffer) => any) {
        this.maybeHaveNewMailHandler = handler
    }

    private _checkMaybeHaveNewMail(tokens: Buffer[]): void {
        if (!this.maybeHaveNewMailHandler) {
            return
        }
        if (tokens.length < 3) {
            return
        }
        if (tokens[2].toString().toUpperCase() != "EXISTS") {
            return
        }
        this.maybeHaveNewMailHandler(this.lastSelectedMbox || Buffer.alloc(0))
    }

    // 协议的最后一行返回
    getLastReadedBuffer(): Buffer {
        return this.lastReadedBuffer
    }

    // 是否网络错误
    isNetError() {
        return this.errorFlag
    }

    // 是否逻辑错误
    isLogicError() {
        return this.logicErrorFlag
    }

    // 是否密码错误
    isPasswordError() {
        return this.passwordErrorFlag
    }

    escapeBuffer(bf: Buffer): Buffer {
        let needSize = false
        let needQuote = false
        let r = Buffer.alloc(bf.length * 2 + 16)
        let rlen = 0
        let i;
        r[rlen++] = this.char_quot
        for (i = 0; i < bf.length; i++) {
            let ch = bf[i]
            if (ch < 32) {
                needSize = true
                break
            }
            switch (ch) {
                case this.char_blank:
                case '{'.charCodeAt(0):
                    needQuote = true
                    break
                case this.char_quot:
                    needQuote = true
                    r[rlen++] = this.char_backslash
                    break
            }
            r[rlen++] = ch
        }
        r[rlen++] = this.char_quot
        if (needSize) {
            let prefix = Buffer.from("{" + bf.length + "}\r\n")
            return Buffer.concat([prefix, bf])
        }
        if (needQuote) {
            return r.subarray(0, rlen)
        }
        if (r.length == 2) {
            return r
        }
        return bf
    }

    escapeString(str: string): string {
        return this.escapeBuffer(Buffer.from(str)).toString()
    }

    // 按imap协议,编译(转义)数据
    escape(str: string | Buffer): string | Buffer {
        if (Buffer.isBuffer(str)) {
            return this.escapeBuffer(str)
        } else {
            return this.escapeBuffer(Buffer.from(str)).toString()
        }
    }

    // 读一行返回,并解析为 tokens
    async readOneLineTokens() {
        const result = await this.socket.gets(1024 * 1024 * 10)
        if (result === null) {
            this.errorFlag = true
            this.needCloseConnection = true
            return null
        }
        let ret: ReadOneLineResult = {
            tokens: [], extraDataLength: -1
        }
        let last_quoted = false
        let bf = result
        let blen = bf.length
        let i = 0
        let ch: number
        if (blen > 0 && bf[blen - 1] == this.char_newline) {
            blen--
            bf = bf.subarray(0, blen)
        }
        if (blen > 0 && bf[blen - 1] == "\r".charCodeAt(0)) {
            blen--
            bf = bf.subarray(0, blen)
        }
        if (this.readWriteRecorder) {
            this.readWriteRecorder("read", bf)
        }
        this.lastReadedBuffer = bf
        if (this.debugMode) {
            console.log("imap read :", bf.toString())
        }
        while (i < blen) {
            let tmpbf = Buffer.allocUnsafe(bf.length + 1)
            let tmpbf_i = 0
            ch = this.char_blank
            while (i < blen) {
                ch = bf[i++]
                if (ch != this.char_blank) {
                    break
                }
            }
            if (ch == this.char_blank) {
                continue
            }
            if (ch == this.char_quot) {
                last_quoted = true
                while (i < blen) {
                    ch = bf[i++]
                    if (ch == this.char_quot) {
                        ret.tokens.push(tmpbf.subarray(0, i))
                        tmpbf_i = 0
                        break
                    } else if (ch == this.char_backslash) {
                        if (i == blen) {
                            this.needCloseConnection = true
                            this.logicErrorFlag = true
                            return null
                        }
                        ch = bf[i++]
                        tmpbf.writeUint8(ch, tmpbf_i++)
                    } else {
                        tmpbf.writeUint8(ch, tmpbf_i++)
                    }
                }
            } else {
                last_quoted = false
                tmpbf.writeUint8(ch, tmpbf_i++)
                while (i < blen) {
                    ch = bf[i++]
                    if (ch == this.char_blank) {
                        break
                    } else {
                        tmpbf.writeUint8(ch, tmpbf_i++)
                    }
                }
                ret.tokens.push(tmpbf.subarray(0, tmpbf_i))
                tmpbf_i = 0
            }
        }
        //
        if (this.needCloseConnection) {
            return null
        }
        // last token maybe is {123}
        do {
            if (last_quoted) {
                break
            }
            if (ret.tokens.length == 0) {
                break
            }

            let lastbf = ret.tokens[ret.tokens.length - 1]
            if (lastbf.length < 3) {
                break
            }
            if (lastbf[0] != "{".charCodeAt(0) || lastbf[lastbf.length - 1] != "}".charCodeAt(0)) {
                break
            }
            let l = parseInt(lastbf.subarray(1, lastbf.length - 1).toString())
            if (l < 0) {
                this.needCloseConnection = true
                this.logicErrorFlag = true
                return null
            }
            ret.extraDataLength = l
            ret.tokens.pop()
        } while (0)

        //
        return ret
    }

    // 读取一个完整的返回, 并解析为 tokens
    async readTokens() {
        let tokens: Buffer[] = []
        while (1) {
            let tmpret = await this.readOneLineTokens()
            if (tmpret === null) {
                return null
            }
            tokens = tokens.concat(tmpret.tokens)
            if (tmpret.extraDataLength < 0) {
                break
            }
            if (tmpret.extraDataLength == 0) {
                tokens.push(Buffer.allocUnsafe(0))
            } else {
                let tmpbuf = await this.socket.readn(tmpret.extraDataLength)
                if (tmpbuf === null) {
                    this.errorFlag = true
                    this.needCloseConnection = true
                    return null
                }
                tokens.push(tmpbuf)
            }
        }
        return tokens
    }

    // 解析 返回结果是不是 OK/NO/BAD
    parseResult(tokens: Buffer[]): boolean {
        if (tokens.length < 2 || tokens[1].length < 2) {
            this.needCloseConnection = true
            this.logicErrorFlag = true
            return false
        }
        let ch = tokens[1].subarray(0, 1).toString().toUpperCase()
        if (ch == "O") {
            this.resultStatus = "O"
        } else if (ch == "N") {
            this.resultStatus = "N"
        } else if (ch == "B") {
            this.resultStatus = "B"
        } else {
            this.needCloseConnection = true
            this.logicErrorFlag = true
            return false
        }
        return true
    }
    resultIsOk() {
        return this.resultStatus == "O"
    }
    resultIsNo() {
        return this.resultStatus == "N"
    }
    resultIsBad() {
        return this.resultStatus == "B"
    }

    // 通用 命令 封装
    async generalCmd(cmdArgv: (Buffer | string)[], options?: {
        callbackForUntag?: { (data: Buffer[]): void }
        callbackForTag?: { (data: Buffer[]): void }
        [keys: string]: any
    }) {
        let tag_id = "" + (this.tag_id++);
        let writeBuffers: Buffer[] = []
        writeBuffers.push(Buffer.from(tag_id + " "))
        cmdArgv.forEach(arg => {
            if (Buffer.isBuffer(arg)) {
                writeBuffers.push(arg)
            } else {
                writeBuffers.push(Buffer.from(arg))
            }
        })
        let cmdLine = Buffer.concat(writeBuffers)
        await this.socket.writeBuffer(cmdLine)
        await this.socket.write("\r\n")
        if (this.debugMode) {
            console.log("imap write:", cmdLine.toString())
        }
        if (this.readWriteRecorder) {
            this.readWriteRecorder("write", cmdLine)
        }
        if (options === undefined) {
            options = { callbackForUntag: undefined, callbackForTag: undefined }
        }
        while (1) {
            const tokens = await this.readTokens()
            if (tokens === null) {
                return null
            }
            if (tokens.length < 2) {
                this.needCloseConnection = true
                this.logicErrorFlag = true
                return null
            }
            let token = tokens[0].toString()
            if (token == "*") {
                if (options.callbackForUntag) {
                    options.callbackForUntag(tokens)
                }
                continue
            }
            if (token != tag_id) {
                continue
            }
            if (!this.parseResult(tokens)) {
                return null
            }
            if (options.callbackForTag) {
                options.callbackForTag(tokens)
            }
            if (this.resultIsOk()) {
                return true
            }
            return false
        }
        return true
    }

    // 发起 ssl 连接握手
    async cmdStartTLS() {
        let res = await this.generalCmd(["STARTTLS"], {
            callbackForUntag: async (tokens: Buffer[]) => {
            },
        })
        if (res === null) {
            return null
        }
        if (res === false) {
            return false
        }
        res = await this.socket.tlsConnect()
        if (res === null) {
            return null
        }
        if (res === false) {
            return false
        }
        return true
    }

    // welcome
    async readWelcome() {
        let tokens = await this.readTokens()
        if (tokens === null) {
            return null
        }
        return true
    }

    // 命令 capability
    async getCapability() {
        if (this.capability.length > 0) {
            return this.capability
        }
        return this.forceGetCapability()
    }

    async forceGetCapability() {
        let r = await this.generalCmd(["CAPABILITY"], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens[1].toString().toUpperCase() == "CAPABILITY") {
                    this.capability = " " + tokens.slice(2).map(b => b.toString()).join(" ").toUpperCase() + " "
                }
            },
        })
        if (r === null) {
            return r
        }
        return this.capability
    }

    // 登录
    async login() {
        return this.generalCmd([("LOGIN " + this.escapeString(this.user) + " " + this.escapeString(this.pass))], {
            callbackForTag: (tokens: Buffer[]) => {
                if (tokens.length > 2 && tokens[2].toString().toUpperCase() == "[CAPABILITY") {
                    this.capability = " " + tokens.slice(2).map(b => b.toString()).join(" ").toUpperCase() + " "
                }
            },
        })
    }

    // 命令 ID, id 后面的具体内容 可以在 options 中设置
    async cmdId(idInfo?: string) {
        if (idInfo === undefined || idInfo == "") {
            idInfo = this.cmdIdInfo
        }
        if (idInfo == "") {
            idInfo = '("name" "linuxmail.cn")'
        }
        return this.generalCmd(["ID " + idInfo])
    }

    // 命令 LIST/LSUB
    private async _cmdListOrLsub(listOrLsub: string) {
        let foundInbox = false
        let mboxs: MboxInfo[] = []
        let res = await this.generalCmd([listOrLsub + " \"\" *"], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 4) {
                    return
                }
                let mboxName = tokens[tokens.length - 1]
                let info: MboxInfo = {
                    mboxName,
                    mboxNameUtf8: "",
                    attrs: {
                        noinferiors: false,
                        noselect: false,
                        hierarchySplitor: tokens[tokens.length - 2].toString()[0],
                        inbox: false,
                        junk: false,
                        trash: false,
                        sent: false,
                        drafts: false,
                    }
                }
                if (!foundInbox) {
                    if (mboxName.toString().toLowerCase() == "inbox") {
                        info.attrs.inbox = true
                        foundInbox = true
                    }
                }
                mboxs.push(info)
                if (tokens.length == 4) {
                    return
                }
                tokens.splice(2, tokens.length - 4).forEach(tokenBuffer => {
                    let s = tokenBuffer.toString().replace("(", "").replace(")", "").replace("\\", "").toLowerCase()
                    if (s in info.attrs) {
                        (info.attrs as any)[s] = true
                    }
                })
            },
        })
        if (!res) {
            return null
        }
        return mboxs
    }

    async getMboxList() {
        return this._cmdListOrLsub("LIST")
    }

    async getSubscribedMboxList() {
        return this._cmdListOrLsub("LSUB")
    }

    // 命令 status
    async getMboxStatus(mboxName: string | Buffer, items?: string) {
        // S status abc (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)
        // * STATUS abc (MESSAGES 123 RECENT 1 UIDNEXT 328 UIDVALIDITY 166 UNSEEN 9)
        // S OK Status completed (0.001 + 0.000 secs).
        let status: MboxStatus = { messages: -1, recent: 0, uidnext: -1, uidvalidity: -1, unseen: 0, }
        if (items === undefined) {
            items = "(MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)"
        }
        let res = await this.generalCmd(["STATUS ", this.escape(mboxName), (" " + items)], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 5) {
                    return
                }
                let i;
                for (i = 3; i < tokens.length; i += 2) {
                    if (i + 1 == tokens.length) {
                        break
                    }
                    let key = tokens[i].toString().replace("(", "").replace(")", "").toLowerCase()
                    let val = parseInt(tokens[i + 1].toString())
                    if (key in status) {
                        (status as any)[key] = val
                    }
                }
            },
        })
        if (res === null) {
            return null
        }
        if (status.messages < 0) {
            return false
        }
        return status
    }

    // 获取所有文件夹的详细信息
    async getAllMboxInfos() {
        // imap mbox list
        let mboxs = await this.getMboxList()
        if (mboxs === null) {
            return null
        }
        // imap mbox lsub
        const lsub = await this.getMboxList()
        if (lsub === null) {
            return null
        }
        // who subscribed?
        let lsubSet: any = {}
        lsub.forEach(f => {
            lsubSet[f.mboxName.toString("binary")] = true
        })
        mboxs.forEach(f => {
            if (f.mboxName.toString("binary") in lsubSet) {
                f.subscribed = true
            }
        })
        // imap fodler status one by one
        let i;
        for (i = 0; i < mboxs.length; i++) {
            let fo = mboxs[i]
            let status = await this.getMboxStatus(fo.mboxName)
            if (status) {
                fo.status = status
            }
        }
        return mboxs
    }

    // 命令 select
    async selectMbox(mboxName: string | Buffer) {
        let infos: MboxSelect = {
            exists: -1,
            recent: 0,
            uidvalidity: -1,
            uidnext: -1,
            highestmodseq: -1
        }
        if (!Buffer.isBuffer(mboxName)) {
            mboxName = Buffer.from(mboxName)
        }
        if (mboxName.equals(this.lastSelectedMbox)) {
            return this.lastSelectedResult || infos
        }

        this.lastSelectedMbox = Buffer.alloc(0)
        let res = await this.generalCmd(["SELECT ", this.escape(mboxName)], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 3) {
                    return
                }
                let key = tokens[2].toString().toLowerCase()
                if (key[0] == "") {
                    return
                }
                if (key[0] == "[") {
                    if (tokens.length < 4) {
                        return
                    }
                    if (key.substring(1) in infos) {
                        (infos as any)[key.substring(1)] = parseInt(tokens[3].toString().replace("]", ""))
                    }
                    return
                } else {
                    if (key in infos) {
                        (infos as any)[key] = parseInt(tokens[1].toString())
                    }
                }
            },
        })
        if (!res) {
            return null
        }
        if (infos.exists == -1) {
            return false
        }
        this.lastSelectedMbox = mboxName
        this.lastSelectedResult = infos
        return infos
    }

    // 命令 select, 强制执行 select
    async forceSelectMbox(mboxName: string | Buffer) {
        this.lastSelectedMbox = Buffer.allocUnsafe(0)
        return this.selectMbox(mboxName)
    }

    // 解析 邮件标记
    private _decode_mail_flags(tokens: Buffer[]): MailFlags {
        let flags: MailFlags = {
            answered: false, seen: false, draft: false, flagged: false, deleted: false
        }
        let stop = false
        let i = 0;
        for (i = 0; i < tokens.length; i++) {
            if (stop) {
                break
            }
            let key = tokens[i].subarray(0, 32).toString()
            if (key.length && key[0] == "(") {
                key = key.substring(1)
            }
            if (key.length && key[0] == "\\") {
                key = key.substring(1)
            }
            if (key.length && key[key.length - 1] == ")") {
                stop = true
                key = key.substring(0, key.length - 1)
            }
            if (key.length && key[key.length - 1] == ")") {
                key = key.substring(0, key.length - 1)
            }
            if (key == "") {
                continue
            }
            key = key.toLowerCase()
            if (key in flags) {
                (flags as any)[key] = true
            }
        }
        return flags
    }

    // 编码 邮件标记
    private _encode_mail_flags(flags: MailFlags): string {
        let fs: string[] = [];
        if (flags.answered === true) {
            fs.push("\\Answered")
        }
        if (flags.seen === true) {
            fs.push("\\Seen")
        }
        if (flags.draft === true) {
            fs.push("\\Draft")
        }
        if (flags.flagged === true) {
            fs.push("\\Flagged")
        }
        if (flags.deleted === true) {
            fs.push("\\Deleted")
        }
        return "(" + fs.join(" ") + ")"
    }

    // 获取邮件列表 UID + 标记
    async fetchUidListWithFlags() {
        if (!this.lastSelectedResult || this.lastSelectedResult.exists == 0) {
            return []
        }
        let uids: MailUidWithFlags[] = []
        let res = await this.generalCmd(["FETCH 1:* (UID FLAGS)"], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 6 || tokens[3].toString().toUpperCase() != "(UID") {
                    return
                }
                const uid = parseInt(tokens[4].toString())
                const flags = this._decode_mail_flags(tokens.slice(6))
                uids.push({ uid, ...flags })
            },
        })
        if (!res) {
            return null
        }
        return uids
    }

    //  s search cc a
    // * SEARCH 9
    // * 13 FETCH (FLAGS (\Seen))
    // * 15 FETCH (FLAGS (\Seen))
    // * 18 EXISTS
    // s OK Search completed (0.001 + 0.000 secs).
    async searchMail(querys: string) {
        let that = this
        let uids: number[] = []
        let res = await this.generalCmd(["UID SEARCH ", querys], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 3) {
                    return
                }
                that._checkMaybeHaveNewMail(tokens)
                if (tokens[1].toString().toUpperCase() != "SEARCH") {
                    return
                }
                for (let i = 2; i < tokens.length; i++) {
                    uids.push(parseInt(tokens[i].toString()))
                }
            },
        })
        if (!res) {
            return null
        }
        return uids
    }

    // 通过搜索命令, 获取当前文件夹, 全部邮件
    async searchAllUids() {
        return this.searchMail("ALL")
    }
    // 通过搜索命令, 获取当前文件夹, 全部未读邮件
    async searchUnseenUids() {
        return this.searchMail("UNSEEN")
    }
    // 通过搜索命令, 获取当前文件夹, 全部已回复邮件
    async searchAnsweredUids() {
        return this.searchMail("ANSWERED")
    }
    // 通过搜索命令, 获取当前文件夹, 全部设置了已删除标记的邮件
    async searchDeletedUids() {
        return this.searchMail("DELETED")
    }
    // 通过搜索命令, 获取当前文件夹, 全部草稿邮件
    async searchDraftUids() {
        return this.searchMail("DRAFT")
    }
    // 通过搜索命令, 获取当前文件夹, 全部flagged(星标)邮件
    async searchFlaggedUids() {
        return this.searchMail("FLAGGED")
    }

    // 获取邮件信体和标记
    async fetchOneMailByUid(uid: number,
        callbackForMailPieceData: { (pieceData: Buffer): Promise<boolean> },
        options?: {
            callbackForFlags?: { (flags: MailFlags): void }
        }) {
        let that = this
        let tag_id = "" + (that.tag_id++);
        const cmd = tag_id + " UID FETCH " + uid + " (UID FLAGS BODY.PEEK[])"
        await that.socket.write(cmd + "\r\n")
        if (that.debugMode) {
            console.log("imap write:", cmd)
        }
        if (that.readWriteRecorder) {
            that.readWriteRecorder("write", Buffer.from(cmd))
        }

        async function maybeReadExtra(left: number) {
            if (left < 0) {
                return true
            }
            while (left > 0) {
                let rlen = (left > 4096) ? 4096 : left
                const data = await that.socket.readn(rlen)
                if (data === null) {
                    return null
                }
                left -= data.length
            }
            if (await that.readTokens() === null) {
                return null
            }
            return true
        }

        while (1) {
            const lineResult = await that.readOneLineTokens()
            if (lineResult === null) {
                return null
            }
            const tokens = lineResult.tokens
            if (tokens.length < 1) {
                continue
            }
            let token = tokens[0].toString()
            let extraDataLength = lineResult.extraDataLength
            if (token != "*") {
                if (!await maybeReadExtra(extraDataLength)) {
                    return false
                }
                if (token != tag_id) {
                    continue
                }
                if (!that.parseResult(tokens)) {
                    return null
                }
                return true
            }
            if (tokens.length > 2) {
                that._checkMaybeHaveNewMail(tokens)
            }
            let isFetch = false
            if (tokens.length > 3) {
                that._checkMaybeHaveNewMail(tokens)
                if (tokens[3].toString().toUpperCase() == "(UID") {
                    isFetch = true
                }
            }
            if (isFetch) {
                if (options && options.callbackForFlags) {
                    const flags = that._decode_mail_flags(lineResult.tokens.slice(6));
                    options.callbackForFlags(flags)
                }
                if (extraDataLength < 0) {
                    await callbackForMailPieceData(tokens[tokens.length - 1])
                    if (await that.readTokens() === null) {
                        return null
                    }
                    continue
                }

                let left = extraDataLength
                while (left > 0) {
                    let rlen = (left > 4096) ? 4096 : left
                    const data = await that.socket.readn(rlen)
                    if (data === null) {
                        return null
                    }
                    left -= data.length
                    await callbackForMailPieceData(data)
                }
                if (await that.readTokens() === null) {
                    return null
                }
            } else {
                if (!await maybeReadExtra(extraDataLength)) {
                    return false
                }
            }
            continue
        }
        return true
    }

    // 命令 create, 创建文件夹
    async createMbox(mboxName: string | Buffer) {
        let r = await this.generalCmd(["CREATE ", this.escape(mboxName)])
        if (r === null) {
            return null
        }
        if (r == true) {
            return true
        }
        return false
    }

    // 创建文件夹, 并订阅
    async createAndSubscribeMbox(mboxName: string | Buffer) {
        let r = await this.createMbox(mboxName)
        if (r !== true) {
            return r
        }
        return await this.subscribeMbox(mboxName);
    }

    // 命令 delete, 删除文件夹
    async deleteMbox(mboxName: string | Buffer) {
        let r = await this.generalCmd(["DELETE ", this.escape(mboxName)])
        if (r === null) {
            return null
        }
        return true
    }

    // 命令 subscribe, 订阅文件夹
    async subscribeMbox(mboxName: string | Buffer) {
        let r = await this.generalCmd(["SUBSCRIBE ", this.escape(mboxName)])
        return r
    }

    // 命令 unSubscribe, 取消订阅文件夹
    async unSubscribeMbox(mboxName: string | Buffer) {
        let r = await this.generalCmd(["UNSUBSCRIBE ", this.escape(mboxName)])
        return r
    }

    // 命令 rename, 文件夹改名
    async renameMbox(fromMboxName: string | Buffer, toMboxName: string | Buffer) {
        let r = await this.generalCmd(["RENAME ", this.escape(fromMboxName), " ", this.escape(toMboxName)])
        return r
    }

    // 打开imap连接,并认证等
    // 使用者可以自己实现类似的方法
    async open() {
        if (! await this.socket.connect()) {
            return false
        }
        if (! await this.readWelcome()) {
            return false
        }
        if (! await this.getCapability()) {
            return false
        }

        let tlsGo = this.startTLS
        if (!tlsGo && this.tryStartTLS) {
            if (this.capability.indexOf(" STARTTLS ") > -1) {
                tlsGo = true
            }
        }
        if (tlsGo) {
            let r = await this.cmdStartTLS()
            if (r === null) {
                this.errorFlag = true
                this.needCloseConnection = true
                return null
            }
            if (!r) {
                return false
            }
        }

        switch (await this.login()) {
            case null:
                return false
            case false:
                return false
            case true:
                break
        }
        if (!this.resultIsOk()) {
            return false
        }

        if (this.capability.indexOf(" ID ") > -1) {
            if (!await this.cmdId()) {
                return false
            }
        }

        return true
    }

    // 命令 logout, 退出登录
    async logout() {
        let r = await this.generalCmd(["LOGOUT"])
        this.socket.close()
        if (r === null) {
            return null
        }
        return true
    }

    // 命令 append, 上传信件
    // A003 APPEND saved-messages (\Seen) {326}
    async appendMail(mboxName: Buffer | string, mailSize: number,
        callbackForMailPieceData: () => Promise<Buffer | null>,
        options?: {
            flags?: MailFlags
            date?: Date,
            callbackForUidplus?: (r: UidplusResult) => any,
        }) {
        if (!Buffer.isBuffer(mboxName)) {
            mboxName = Buffer.from(mboxName)
        }
        options = options || {}
        // cmd
        let tag_id = "" + (this.tag_id++);
        let writeBuffers: Buffer[] = []
        writeBuffers.push(Buffer.from(tag_id + " APPEND "))
        writeBuffers.push(this.escapeBuffer(mboxName))
        if (options.flags) {
            let flags = this._encode_mail_flags(options.flags)
            if (flags != "()") {
                writeBuffers.push(Buffer.from(" "))
                writeBuffers.push(Buffer.from(flags))
            }
        }
        options.date = new Date()
        if (options.date) {
            let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
            let d = options.date
            let r = " \""
            r += d.getDate() + "-" + months[d.getMonth()] + "-" + d.getFullYear() + " "
            r += d.toTimeString().split(" (")[0]
            r += "\""
            writeBuffers.push(Buffer.from(r))
        }
        writeBuffers.push(Buffer.from(" {" + mailSize + "}"))

        let cmdLine = Buffer.concat(writeBuffers)
        await this.socket.writeBuffer(cmdLine)
        await this.socket.write("\r\n")

        if (this.debugMode) {
            console.log("imap write:", cmdLine.toString())
        }
        if (this.readWriteRecorder) {
            this.readWriteRecorder("write", Buffer.from(cmdLine))
        }

        // 读取返回, 期望: 
        // + Ready for literal data
        const tokens = await this.readTokens()
        if (tokens === null) {
            return null
        }
        if (tokens.length < 1) {
            this.logicErrorFlag = true
            return null
        }
        if (tokens[0].toString() != "+") {
            return false
        }

        // 写数据(信件)
        let leftSize = mailSize
        while (leftSize > 0) {
            let bf = await callbackForMailPieceData()
            if (bf === null) {
                this.logicErrorFlag = true
                return null
            }
            leftSize -= bf.length
            if (!this.socket.writeBuffer(bf)) {
                this.errorFlag = true
                return null
            }
        }
        if (!this.socket.write("\r\n")) {
            this.errorFlag = true
            return null
        }
        if (!this.socket.flush()) {
            this.errorFlag = true
            return null
        }

        // 读取返回
        function _parse_append_result(tokens: Buffer[]): UidplusResult {
            let r: UidplusResult = {
                uidvalidity: -1,
                uid: -1
            }
            for (let i = 2; i < tokens.length; i++) {
                let t = tokens[i].toString()
                if (t != "[APPENDUID") {
                    continue
                }
                i++
                if (i == tokens.length) {
                    break;
                }
                r.uidvalidity = parseInt(tokens[i].toString())
                i++
                if (i == tokens.length) {
                    break;
                }
                r.uid = parseInt(tokens[i].toString())
                break
            }
            return r
        }

        while (1) {
            const tokens = await this.readTokens()
            if (tokens === null) {
                return null
            }
            let token = tokens[0].toString()
            if (token == "*") {
                this._checkMaybeHaveNewMail(tokens)
                continue
            }
            if (token != tag_id) {
                continue
            }
            if (!this.parseResult(tokens)) {
                return null
            }
            if (options && options.callbackForUidplus) {
                let r = _parse_append_result(tokens)
                options.callbackForUidplus(r)
            }
            if (this.resultIsOk()) {
                return true
            }
            return false
        }

        //
        return true
    }

    // 解析 move/copy 的返回(uidplus)
    // * OK [COPYUID 1683689640 20:22 33:35] Moved UIDs
    // 只解析 移动/复制 一封信件的情况, 作者认为:
    //    1, 大部分情况,一封一封的移动能满足使用者的需求
    //    2, 如果移动多封或者全部邮件, 使用者大概率不会关注uidplus的结果
    private _parse_move_copy_uidplus_result(tokens: Buffer[]): UidplusResult {
        let r: UidplusResult = {
            uidvalidity: -1,
            uid: -1
        }
        if (tokens.length < 3) {
            return r
        }
        if (tokens[1].length < 2) {
            return r
        }
        if (String.fromCharCode(tokens[1][0]).toUpperCase() != 'O') {
            return r
        }
        if (tokens[2].toString().toUpperCase() != "[COPYUID") {
            return r
        }
        if (tokens.length < 6) {
            return r
        }
        r.uidvalidity = parseInt(tokens[3].toString())
        r.uid = parseInt(tokens[5].toString())
        if (r.uidvalidity == -1 || r.uid == -1) {
            r.uidvalidity = -1
            r.uid = -1
        }
        return r
    }

    private async _move_copy_one_mail(CMD: string, uid: number | string, toMboxName: string | Buffer, options?: {
        callbackForUidplus?: (r: { uidvalidity: number, uid: number }) => any
    }) {
        let uidplus: UidplusResult = {
            uidvalidity: -1,
            uid: -1
        }
        let cmdOptions = {
        }
        if (options && options.callbackForUidplus) {
            cmdOptions = {
                callbackForUntag: (tokens: Buffer[]) => {
                    if (uidplus.uidvalidity == -1) {
                        uidplus = this._parse_move_copy_uidplus_result(tokens)
                    }
                },
                callbackForTag: (tokens: Buffer[]) => {
                    if (uidplus.uidvalidity == -1) {
                        uidplus = this._parse_move_copy_uidplus_result(tokens)
                    }
                },
            }
        }
        let r = await this.generalCmd([CMD + " " + uid + " ", this.escape(toMboxName)], cmdOptions)
        if (options && options.callbackForUidplus) {
            options.callbackForUidplus(uidplus)
        }
        return r
    }

    // 命令 UID MOVE, 移动一封邮件
    async moveOneMailByUid(uid: number | string, toMboxName: string | Buffer, options?: {
        callbackForUidplus?: (r: { uidvalidity: number, uid: number }) => any,
    }) {
        return this._move_copy_one_mail("UID MOVE", uid, toMboxName, options)
    }

    // 命令 MOVE, 移动一封邮件
    async moveOneMailBySn(sn: number | string, toMboxName: string | Buffer, options?: {
        callbackForUidplus?: (r: { uidvalidity: number, uid: number }) => any,
    }) {
        return this._move_copy_one_mail("MOVE", sn, toMboxName, options)
    }

    // 命令 UID MOVE, 移动一组邮件
    async moveMailByUid(uids: string, toMboxName: string | Buffer, options?: {
    }) {
        return this._move_copy_one_mail("UID MOVE", uids, toMboxName, options)
    }

    // 命令 MOVE, 移动一组邮件
    async moveMailBySn(sns: string, toMboxName: string | Buffer, options?: {
    }) {
        return this._move_copy_one_mail("UID MOVE", sns, toMboxName, options)
    }

    // 命令 UID COPY, 复制一封邮件
    async copyOneMailByUid(uid: number | string, toMboxName: string | Buffer, options?: {
        callbackForUidplus?: (r: { uidvalidity: number, uid: number }) => any,
    }) {
        return this._move_copy_one_mail("UID COPY", uid, toMboxName, options)
    }

    // 命令 COPY, 复制一封邮件
    async copyOneMailBySn(sn: number | string, toMboxName: string | Buffer, options?: {
        callbackForUidplus?: (r: { uidvalidity: number, uid: number }) => any,
    }) {
        return this._move_copy_one_mail("COPY", sn, toMboxName, options)
    }

    // 命令 UID COPY, 移动一组邮件
    async copyMailByUid(uids: string, toMboxName: string | Buffer, options?: {
    }) {
        return this._move_copy_one_mail("UID COPY", uids, toMboxName, options)
    }

    // 命令 COPY, 移动一组邮件
    async copyMailBySn(sns: string, toMboxName: string | Buffer, options?: {
    }) {
        return this._move_copy_one_mail("COPY", sns, toMboxName, options)
    }

    //
    private async _set_mail_flag(CMD: string, uid: number | string, flags: MailFlags, set_or_unset: boolean) {
        let fs = this._encode_mail_flags(flags)
        if (fs == "()") {
            return true
        }
        return this.generalCmd([CMD + " " + uid + " ", set_or_unset ? "+" : "-", "FLAGS.SILENT ", fs])
    }
    // 命令 UID STORE, 设置标记
    async setMailFlagByUid(uidOrUids: number | string, flags: MailFlags, set_or_unset?: boolean) {
        return this._set_mail_flag("UID STORE", uidOrUids, flags, (set_or_unset !== false))
    }
    async unsetMailFlagByUid(uidOrUids: number | string, flags: MailFlags) {
        return this._set_mail_flag("UID STORE", uidOrUids, flags, false)
    }

    // 命令 STORE, 设置标记
    async setMailFlagBySn(snOrSns: number | string, flags: MailFlags, set_or_unset?: boolean) {
        return this._set_mail_flag("STORE", snOrSns, flags, (set_or_unset !== false))
    }
    async unsetMailFlagBySn(snOrSns: number | string, flags: MailFlags) {
        return this._set_mail_flag("STORE", snOrSns, flags, false)
    }

    // expunge
    async expunge() {
        let that = this
        let res = await this.generalCmd(["EXPUNGE"], {
            callbackForUntag: (tokens: Buffer[]) => {
                if (tokens.length < 3) {
                    return
                }
                this._checkMaybeHaveNewMail(tokens)
            },
        })
        return res
    }

    // 删除信件, UID
    async deleteMailByUid(uidOrUids: number | string) {
        let r = await this.setMailFlagByUid(uidOrUids, { deleted: true })
        if (r !== true) {
            return r
        }
        r = await this.expunge()
        return r
    }

    // 删除信件
    async deleteMailBySn(snOrSns: number | string) {
        let r = await this.setMailFlagBySn(snOrSns, { deleted: true })
        if (r !== true) {
            return r
        }
        r = await this.expunge()
        return r
    }
}